﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Transactions;
using Abp.Auditing;
using Abp.AutoMapper;
using Abp.Domain.Repositories;
using Abp.Domain.Uow;
using Abp.Extensions;
using Abp.Runtime.Security;
using Abp.Timing;
using Hicap.AuthorizationServer.Core.Caches.Sys;
using Hicap.AuthorizationServer.Core.Caches.Sys.Interface;
using Hicap.AuthorizationServer.Core.Common;
using Hicap.AuthorizationServer.Core.Domains.Sys.Users;
using Hicap.AuthorizationServer.Core.Models.Sys;
using Microsoft.EntityFrameworkCore;

namespace Hicap.AuthorizationServer.Core.Domains.Authorization
{
    public class LogInManager : AuthorizationServerDomainServiceBase, ILogInManager
    {
        public IClientInfoProvider ClientInfoProvider { get; set; }
        private readonly IUsersManager _usersManager;
        private readonly IUnitOfWorkManager _unitOfWorkManager;
        private readonly IRepository<User, int> _userRepository;
        private readonly IRepository<UserLoginAttempt, int> _userLoginAttemptRepository;
        private readonly IUserAuthCache _userAuthCache;

        public LogInManager(
            IUsersManager usersManager,
            IRepository<User, int> userRepository,
            IRepository<UserLoginAttempt, int> userLoginAttemptRepository, IUnitOfWorkManager unitOfWorkManager,
            IUserAuthCache userAuthCache)
        {
            _usersManager = usersManager;
            _userRepository = userRepository;
            _userLoginAttemptRepository = userLoginAttemptRepository;
            _unitOfWorkManager = unitOfWorkManager;
            _userAuthCache = userAuthCache;
        }

        [UnitOfWork]
        public virtual async Task<LoginResult> LoginAsync(string userCode, string plainPassword)
        {
            var result = await LoginAsyncInternal(userCode, plainPassword);
            await SaveLoginAttempt(result, userCode);
            return result;
        }

        private async Task<LoginResult> LoginAsyncInternal(string userCode, string plainPassword)
        {
            if (userCode.IsNullOrEmpty())
            {
                throw new ArgumentNullException(L("NullException{0}", nameof(userCode)));
            }

            if (plainPassword.IsNullOrEmpty())
            {
                throw new ArgumentNullException(L("NullException{0}", nameof(plainPassword)));
            }

            var user = await _usersManager.FindByUserCode(userCode);
            if (user == null)
            {
                return new LoginResult(LoginResultType.InvalidUserCode);
            }

            if (!_usersManager.CheckUserPassword(plainPassword, user))
            {
                return new LoginResult(LoginResultType.InvalidPassword, user);
            }

            return await CreateLoginSucceedResultAsync(user);
        }

        private async Task<LoginResult> CreateLoginSucceedResultAsync(User user)
        {
            user.LastLoginTime = Clock.Now;

            var principal = await CreatePrinciple(user);

            await _userRepository.UpdateAsync(user);

            return new LoginResult(
                user,
                principal.Identity as ClaimsIdentity
            );
        }

        public Task<ClaimsPrincipal> CreatePrinciple(User user)
        {
            var identity = new ClaimsIdentity();
            identity.AddClaim(new Claim(AbpClaimTypes.UserName, user.UserCode));
            identity.AddClaim(new Claim(AbpClaimTypes.UserId, user.Id.ToString()));
            identity.AddClaim(new Claim(ClaimTypes.AuthorizationDecision, GetUserPermission(user.Id)));
            identity.AddClaim(new Claim(ClaimTypes.GroupSid, Convert.ToString(user.SubordinateCompany?.Id)));

            return Task.FromResult(new ClaimsPrincipal(identity));
        }

        protected virtual async Task SaveLoginAttempt(LoginResult loginResult, string userCode)
        {
            using (var uow = _unitOfWorkManager.Begin(TransactionScopeOption.Suppress))
            {
                var loginAttempt = new UserLoginAttempt
                {
                    UserId = loginResult.User != null ? loginResult.User.Id : (int?)null,
                    UserCode = userCode,

                    Result = loginResult.Result,
                    BrowserInfo = ClientInfoProvider.BrowserInfo,
                    ClientIpAddress = ClientInfoProvider.ClientIpAddress,
                    ClientName = ClientInfoProvider.ComputerName,
                };

                await _userLoginAttemptRepository.InsertAsync(loginAttempt);
                await _unitOfWorkManager.Current.SaveChangesAsync();

                await uow.CompleteAsync();
            }
        }

        public string GetUserPermission(int userId)
        {
            var userAuth = _userAuthCache.Get(userId);

            return String.Join(",", userAuth.AuthObjects.Select(x => x.ObjectCode));
        }
    }
}